#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<stdlib.h>
#include<fcntl.h>
#include<ctype.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>

#define MAX 1024//字符串的最大长度
#define ARGC 64//我们所设置的参数的最大个数
#define SEP " "//我们需要以空格为分隔符进行切割

//一般用户自定义的环境变量，在bash中要用户自己来进行维护，不要用一个经常被覆盖的缓冲区来保存环境变量


//切割函数
int split(char* commandstr,char* argv[])
{
    assert(commandstr);
    assert(argv);

    //strtok的使用方式
    argv[0] = strtok(commandstr," ");
    if(argv[0]==NULL) return -1;
    int i=1;
    while((argv[i++] = strtok(NULL," ")));
    //while(1)
    //{
    //    argv[i]=strtok(NULL,SEP);
    //    if(argv[i]==NULL) break;//参数为NULL，表示切割完毕，退出循环
    //    i++;
    //}
    return 0;
}
//打印命令行的每个参数
void debugPrint(char* argv[])
{
    int i=0;
    for(i = 0;argv[i];i++)
    {
        printf("%d:%s\n",i,argv[i]);
    }
}
//显示所有环境变量
void showEnv()
{
    extern char ** environ;
    int i=0;
    for(i=0;environ[i];i++)
    {
        printf("%d:%s\n",i,environ[i]);
    }
}
int main()
{
    //用户自定义的环境变量自己维护
    char myenv[32][256];
    int env_index=0;
    int last_exit_code=0;
    int flag=0;//0 > 1>> 2<
    while(1)
    {
        char commandstr[MAX] = {0};//用于存储我们在命令行输入的字符串，获取了字符串后才能进行解析
        char* argv[ARGC] = {NULL};//存储所我们所切割的字符串参数
        printf("[zxn@mymachine curpath]# ");
        fflush(stdout);
        //获取字符串到commandstr
        char* s = fgets(commandstr,sizeof(commandstr),stdin);//会提取我们输入字符串结束时按下的换行
        assert(s);
        (void)s;//由于在某些环境中，如果我们定义了变量但是我们没有使用
        //编译器是会报错的，所以这句代码是保证在release方式发布的时候，若去掉assert，就导致s没有被使用，而带来的编译告警，什么都没做，但充当了一次使用
        commandstr[strlen(commandstr)-1] = '\0';//消除结尾的换行

        //实现重定向功能
        char* start=commandstr;
        while(*start)
        {
            if(*start=='>')//第一个：输出重定向
            {
                flag=0;
                *start='\0';
                start++;
                if(*start=='>')//出现第二个>,就是追加重定向
                {
                    flag=1;
                    start++;
                }
                break;
            }

            if(*start=='<')
            {
                flag=2;
                *start='\0';
                start++;
                break;
            }
            start++;
        }

        if(*start!='\0')
        {
            while(isspace(*start))//跳过重定向后面的空格
            {
                start++;
            }
        }
        else 
        {
            start=NULL;
        }

        //切割字符串
        int n = split(commandstr,argv);
        if(n!=0) continue;

        //debugPrint(argv);
        //version2:增加几个细节
        //1.带颜色
        if(strcmp(argv[0],"ls")==0)
        {
            int pos=0;
            while(argv[pos]) pos++;
            argv[pos++]=(char*)"--color=auto";
            argv[pos]=NULL;//比较安全的做法
        }
        //cd ../cd /:让bash自己执行命令，我们称之为内建命令/内置命令
        if(strcmp(argv[0],"cd")==0)
        {
            //我们在版本1上执行cd命令时，会发现cd到的路径都是父进程所在的文件路径
            //这个路径不会发生变化，那是因为我们是使用子进程去执行这个命令的
            //所以我们要让bash自己执行命令
            if(argv[1]!=NULL) chdir(argv[1]);
            continue;//chdir用于将当前工作目录更改为指定目录
        }
        //设置导入用户自定义环境变量
        //不能之间调用putenv(argv[1])，因为会存在缓冲区的覆盖问题
        //记住不要将环境变量放在一个会变化的数组中即可
        //其实我们之前学习到的几乎所有的环境变量，都是内建命令
        if(strcmp(argv[0],"export")==0)
        {
            if(argv[1]!=NULL)
            {
                strcpy(myenv[env_index],argv[1]);//将命令行参数的字符串全部拷贝到myenv中
                putenv(myenv[env_index++]);
            }
            continue;

        }
        if(strcmp(argv[0],"env")==0)
        {
            showEnv();
            continue;
        }
        if(strcmp(argv[0],"echo")==0)
        {
            //echo $PATH
            const char* target_env=NULL;
            if(argv[1][0]=='$')
            {
                if(argv[1][1] == '?') 
                {
                    printf("%d\n",last_exit_code);
                    continue;
                }
                else 
                    target_env = getenv(argv[1]+1);//获取$之后的环境变量
            }
            if(target_env!=NULL)
                printf("%s = %s\n",argv[1]+1,target_env);

            continue;
        }
        //version1
        pid_t id = fork();
        assert(id>=0);
        (void)id;

        if(id==0)
        {
            //child
            if(start!=NULL)
            {
                if(flag==0){
                    //输出重定向:以写的方式打开文件，并清空内容
                    int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664);
                    if(fd<0)
                    {
                        perror("open error");
                        exit(2);
                    }
                    close(1);
                    dup2(fd,1);
                }
                else if(flag==1)
                {
                    int fd = open(start, O_WRONLY | O_CREAT | O_APPEND,0664);
                    if(fd<0)
                    {
                        perror("open error");
                        exit(2);
                    }
                    close(1);
                    dup2(fd,1);//重定向
                }
                else 
                {
                    int fd = open(start,O_RDONLY);//以读的方式打开文件
                    if(fd<0)
                    {
                        perror("open error");
                        exit(2);
                    }
                    close(0);
                    dup2(fd,0);
                }
            }
            execvp(argv[0],argv);
            exit(1);
        }
        int status=0;
        pid_t ret = waitpid(id,&status,0);
        //等待成功
        if(ret>0)
        {
            last_exit_code = WEXITSTATUS(status);
        }

        //printf("%s\n",commandstr);
    }
    return 0;
}
